/**
 * Class: Poker_Face
 *
 * Purpose:
 *
 * Created: Jan 20, 2010 @ 11:42:18 AM
 * @author Ken Scott
 */
package org.openfantasia.demo;

import org.openfantasia.ps.Space;
import org.openfantasia.ps.actor.Actor;
import org.openfantasia.ps.modifier.Behavior;
import org.openfantasia.ps.particle.Activity;
import org.openfantasia.ps.particle.Chain;
import org.openfantasia.ps.particle.ChainLink;
import org.openfantasia.ps.particle.Particle;
import org.openfantasia.ps.shape.*;
import org.openfantasia.songanalyst.SongObserver;
import org.openfantasia.time.Tickable;
import org.openfantasia.ui.PColor;
import org.openfantasia.util.listener.Event;
import org.openfantasia.util.listener.FloatScalar;
import org.openfantasia.util.listener.Listener;
import org.openfantasia.util.math.DPoint3;
import org.openfantasia.util.math.PMath;
import org.openfantasia.util.math.RotationMatrix;
import org.openfantasia.util.math.transform.Transition;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class Poker_Face extends SongObserver implements Listener {

	private static final String AUDIO_FILE = "C:/ART TOOLS/JavaProcessing/data/pokerface.mp3";

	public static final String VIDEO_OUTPUT_DIRECTORY = "C:/ART TOOLS/JavaProcessing/video/";

	private static final String VIDEO_OUTPUT_FILE = null;	// VIDEO_OUTPUT_DIRECTORY + "pokerface-" + DateTime.COMPACT_DATE_TIME_FORMAT.format(new Date()) + ".mov";

	private static final String EVENT_RECORDING_FILE = null; // "C:/ART TOOLS/JavaProcessing/data/output/pokerface-" + DateTime.FULL_DATE_TIME_FORMAT.format(new Date()) + ".evt";

	private static final String EVENT_PLAYBACK_FILE = null; // "C:/ART TOOLS/JavaProcessing/data/output/pokerface.evt";

	private static final boolean LOCK_CAMERA = true;

	private static final boolean PRESENT = false;

	private static final int EXTENT = 1500;

	private static final double WHEEL_RADIUS = EXTENT / 2.0;

	private static final DPoint3 WHEEL_SIZE = new DPoint3(WHEEL_RADIUS, WHEEL_RADIUS, 0);

	private static final int CHAIN_COUNT = 11;

	private static final double CHAIN_LINK_SIZE = 100;

	private static final double CHAIN_LINK_SPACING = 100;

	private static final int CHAIN_LINK_COUNT = 7;

	private static final int COG_SIZE = 125;

	private static final int STOMPER_SIZE = 60;

	private static final int STOMPER_COLUMN_COUNT = 30;

	private static final int STOMPER_ROW_COUNT = 30;

	private static final double PIP_SIZE = WHEEL_RADIUS * 0.8;

	private static final double POKER_SIZE = WHEEL_RADIUS * 0.8;

	private static final double NOBODY_SIZE = WHEEL_RADIUS;

	private static final int ROW_SIZE = 5;

	private static final int COG_COUNT = 36;


	public static void main(String args[]) {
		if (PRESENT) {
			SongObserver.main(new String[]{"--present", Poker_Face.class.getName()});
		}
		else {
			SongObserver.main(new String[]{"--location=0,0", Poker_Face.class.getName()});
		}
	}


	public static final double BEAT = 0.5042;

	public static final double QUICK_BEAT = BEAT / 2.0;

	public static final double[] TIMELINE = {
			0.0,					// start
			(8.0 * BEAT),	// grind
			16.133403,		// dance beat 1
			24.420084,		// verse 1 start
			40.335012,		// ho ho wo wo 1
			72.865099,		// p-p-p-poker 1
			76.899947,		// p-p-p-poker 2
			54.667524,		// glide 1
			56.468934,		// chorus 1 start
			111.138503,		// glide 2
			112.939503,		// chorus 2 start
			137.156,			// squiggle start
			144.147,			// rap start
			161.360,			// squiggle end
			161.602,			// rap end
			167.610662,		// glide 3
			169.410662,		// chorus 3 start
			233.13,				// last ma
			235.990				// end
	};

	private static int TX = 0;

	public static final double START_OF_SONG = TIMELINE[TX++];

	public static final double GRIND = TIMELINE[TX++];

	public static final double DANCE_BEAT_1 = TIMELINE[TX++];

	public static final double VERSE_1_START = TIMELINE[TX++];

	public static final double HO_HO_WO_WO_1 = TIMELINE[TX++];

	public static final double P_P_P_POKER_1 = TIMELINE[TX++];

	public static final double P_P_P_POKER_2 = TIMELINE[TX++];

	public static final double GLIDE_1 = TIMELINE[TX++];

	public static final double CHORUS_1_START = TIMELINE[TX++];

	public static final double GLIDE_2 = TIMELINE[TX++];

	public static final double CHORUS_2_START = TIMELINE[TX++];

	public static final double SQUIGGLE_START = TIMELINE[TX++];

	public static final double RAP_START = TIMELINE[TX++];

	public static final double SQUIGGLE_END = TIMELINE[TX++];

	public static final double RAP_END = TIMELINE[TX++];

	public static final double GLIDE_3 = TIMELINE[TX++];

	public static final double CHORUS_3_START = TIMELINE[TX++];

	public static final double LAST_MA = TIMELINE[TX++];

	public static final double END_OF_SONG = TIMELINE[TX++];

	public static final double[] MA_MA = new double[]{
			7.297772,
			11.337242,
			15.383435,
			19.3,
			23.3,
			75.9,
			79.9,
			132.4,
			136.41,
			140.42,
			144.43,
			221.1,
			225.11,
			229.12,
			233.13
	};

	public static final double[] PPPPPOKERFACE = new double[]{
			72.865099,
			76.899947,
			129.31,
			133.32,
			210.24,
			214.25,
			218.26,
			222.27,
			226.28,
			230.29,
	};

	public static final double PPPPPOKERFACE_DURATION = 12 * QUICK_BEAT;

	public static final double[] GLIDE = new double[]{
			54.45,
			110.92,
			159.49,
			167.610662,
			183.54,
			199.84,
	};

	public static final double GLIDE_DURATION = 1.791;

	public static final double[] LOVE_NOBODY = new double[]{
			62.527,
			70.595,
			119.030,
			127.100,
			167.357,
			175.427,
			183.467,
			191.588,
			199.683,
			207.698,
			215.819,
	};

	public static final double LOVE_NOBODY_BEAT = BEAT;

	public static final double LOVE_NOBODY_DURATION = 4 * BEAT;

	public static final double[][] STOMPER_PAUSE = new double[][]{
			new double[]{54.45, 56.468934},
			new double[]{110.92, 112.93},
			new double[]{160.85, 169.42},
			new double[]{234.13, END_OF_SONG},
	};

	public static final double[][] LYRICS = new double[][]{
			new double[]{24.473, 27.753},
			new double[]{28.238, 31.518},
			new double[]{32.272, 35.821},
			new double[]{36.306, 38.855},
			new double[]{80.949, 84.311},
			new double[]{85.956, 88.264},
			new double[]{88.977, 92.298},
			new double[]{92.796, 96.830},
			new double[]{144.713, 161.441},
	};

	public static final double LYRICS_BEAT = QUICK_BEAT;

	public static final double[][] OHOH = new double[][]{
			new double[]{40.340, 44.831},
			new double[]{48.408, 52.940},
			new double[]{96.830, 101.106},
			new double[]{104.898, 109.402},
	};

	public static final double OHOH_BEAT = BEAT;

	public static final double[][] HOT = new double[][]{
			new double[]{44.616, 47.924},
			new double[]{52.684, 55.992},
			new double[]{101.106, 104.387},
			new double[]{109.174, 112.952},
	};

	public static final double HOT_BEAT = BEAT;

	// FIELDS

	private Space space = new Space(this);

	private final List<Actor> renderers = new ArrayList<Actor>();

	Spheric ball = new Spheric(this) {
		private double power = 0;


		public void setPower(double power) {
			this.power = power;
		}


		public void receive(Event event) {
			if (event.getChannel().equals(LIVE_AVERAGE_RMS)) {
				power = ((FloatScalar) event).getValue();
			}
		}


		public void setup() {
			addBehavior(Behavior.ORIENTATION, new Behavior(null, null) {
				public Object getValue(Particle particle, Double time, Object param) {
					DPoint3 orientation = (DPoint3) param;
					double rot = (Math.PI / 16 * power);
					return orientation.add(new DPoint3(rot, rot, 0));
				}
			});

		}
	};

	Cube stomper = new Cube(this) {
		private double power = 0;


		public void setPower(double power) {
			this.power = power;
		}


		public void receive(Event event) {
			if (event.getChannel().equals(LIVE_AVERAGE_RMS)) {
				power = ((FloatScalar) event).getValue();
			}
		}


		public void setup() {

			addBehavior(Behavior.VISIBILITY, new Behavior(DANCE_BEAT_1, END_OF_SONG) {
				public Object getValue(Particle particle, Double time, Object param) {
					boolean visibility = (Boolean) param;
					double dt = getBehaviorTimeFraction(particle, time);
					double maxDist = PMath.sqrt(PMath.sq((double) (STOMPER_COLUMN_COUNT + 1) / 2.0) + PMath.sq((double) STOMPER_ROW_COUNT + 1));
					double curDist = dt * maxDist + 1;
					double dist = PMath.dist((double) STOMPER_COLUMN_COUNT / 2.0, STOMPER_ROW_COUNT + 1, (Integer) particle.getAttribute("x"), (Integer) particle.getAttribute("z"));
					boolean viz = dist < curDist;
					return visibility && viz;
				}
			});

			addBehavior(Behavior.SIZE, new Behavior(DANCE_BEAT_1, END_OF_SONG) {
				public Object getValue(Particle particle, Double time, Object param) {
					DPoint3 size = (DPoint3) param;
					double noise = (Double) particle.getAttribute("noise");
					double dt = getBehaviorTime(particle, time);
					double absin = 5 * noise * PMath.abs(PMath.sin(dt * TWO_PI / BEAT));
					return size.mul(new DPoint3(1, absin, 1));
				}
			});

			Behavior lyricsPosition = new Behavior() {
				public Object getValue(Particle particle, Double time, Object param) {
					DPoint3 position = (DPoint3) param;
					int x = (Integer) particle.getAttribute("x");
					int z = (Integer) particle.getAttribute("z");
					if ((x == STOMPER_COLUMN_COUNT / 2) && (z == STOMPER_ROW_COUNT)) {
						double dt = getBehaviorTime(particle, time);
						DPoint3 size = getSize(particle);
						double absin = size.getY() * PMath.abs(PMath.cos(dt * TWO_PI / LYRICS_BEAT));
						position.add(new DPoint3(0, absin, 0));
					}
					return position;
				}
			};

			Behavior lyricsSize = new Behavior() {
				public Object getValue(Particle particle, Double time, Object param) {
					DPoint3 size = (DPoint3) param;
					int x = (Integer) particle.getAttribute("x");
					int z = (Integer) particle.getAttribute("z");
					if ((x == STOMPER_COLUMN_COUNT / 2) && (z == STOMPER_ROW_COUNT)) {
						double dt = getBehaviorTime(particle, time);
						double absin = 2 * size.getY() * PMath.abs(PMath.cos(dt * TWO_PI / LYRICS_BEAT));
						size.add(new DPoint3(0, absin, 0));
					}
					return size;
				}
			};

			for (double[] t : LYRICS) {
				addBehavior(Behavior.POSITION, lyricsPosition.copy(t[0], t[1]));
				addBehavior(Behavior.SIZE, lyricsSize.copy(t[0], t[1]));
			}

			for (double[] t : HOT) {
				addBehavior(Behavior.POSITION, lyricsPosition.copy(t[0], t[1]));
				addBehavior(Behavior.SIZE, lyricsSize.copy(t[0], t[1]));
			}

			Behavior ohohPosition = new Behavior() {
				public Object getValue(Particle particle, Double time, Object param) {
					DPoint3 position = (DPoint3) param;
					double dist = PMath.dist((double) STOMPER_COLUMN_COUNT / 2.0, STOMPER_ROW_COUNT + 1, (Integer) particle.getAttribute("x"), (Integer) particle.getAttribute("z"));
					if (dist <= 3) {
						double dt = getBehaviorTime(particle, time);
						DPoint3 size = getSize(particle);
						double absin = size.getY() * PMath.abs(PMath.cos(dt * TWO_PI / OHOH_BEAT));
						position.add(new DPoint3(0, absin, 0));
					}
					return position;
				}
			};

			Behavior ohohSize = new Behavior() {
				public Object getValue(Particle particle, Double time, Object param) {
					DPoint3 size = (DPoint3) param;
					double dist = PMath.dist((double) STOMPER_COLUMN_COUNT / 2.0, STOMPER_ROW_COUNT + 1, (Integer) particle.getAttribute("x"), (Integer) particle.getAttribute("z"));
					if (dist <= 3) {
						double dt = getBehaviorTime(particle, time);
						double absin = 2 * size.getY() * PMath.abs(PMath.cos(dt * TWO_PI / OHOH_BEAT));
						size.add(new DPoint3(0, absin, 0));
					}
					return size;
				}
			};

			for (double[] t : OHOH) {
				addBehavior(Behavior.POSITION, ohohPosition.copy(t[0], t[1]));
				addBehavior(Behavior.SIZE, ohohSize.copy(t[0], t[1]));
			}

			addBehavior(Behavior.POSITION, new Behavior(CHORUS_3_START, END_OF_SONG) {
				public Object getValue(Particle particle, Double time, Object param) {
					DPoint3 position = (DPoint3) param;
					double maxDist = PMath.sqrt(PMath.sq((double) (STOMPER_COLUMN_COUNT + 1) / 2.0) + PMath.sq((double) STOMPER_ROW_COUNT + 1));
					double dist = PMath.dist((double) STOMPER_COLUMN_COUNT / 2.0, STOMPER_ROW_COUNT + 1, (Integer) particle.getAttribute("x"), (Integer) particle.getAttribute("z"));
					double dt = getBehaviorTimeFraction(particle, time);
					double offset = 2000 * dt * dist / maxDist;
					position.add(new DPoint3(0, offset, 0));
					return position;
				}
			});

			addBehavior(Behavior.POSITION, new Behavior(DANCE_BEAT_1, END_OF_SONG) {
				public Object getValue(Particle particle, Double time, Object param) {
					DPoint3 position = (DPoint3) param;
					double noise = (Double) particle.getAttribute("noise");
					double dt = getBehaviorTime(particle, time);
					double dpt = dt + (BEAT * (2 * (noise - 0.5))) / 50;
					double bval = ((dt % BEAT) / BEAT) - 0.5;
					double meg = SQRT2_2 * 1000;
					double dz = -(2 * meg) + noise * meg * PMath.cos(dpt * PI / BEAT);
					double dy = noise * -5000 * (bval * bval) / 4;
					return position.add(new DPoint3(0, dy, dz));
				}
			});

			addBehavior(Behavior.ORIENTATION, new Behavior(DANCE_BEAT_1, END_OF_SONG) {
				public Object getValue(Particle particle, Double time, Object param) {
					DPoint3 orientation = (DPoint3) param;
					double sign = PMath.sign(particle.getPosition().getX());
					double rot = sign * TWO_PI * time / (BEAT * 4);
					return orientation.add(new DPoint3(0, rot, 0));
				}
			});
		}
	};

	MegaSphere poker = new MegaSphere(this, 1, 20, 60) {

		public boolean isVisible(double time) {
			for (double t : PPPPPOKERFACE) {
				if ((t <= time) && (time < (t + (12 * QUICK_BEAT)))) {
					return true;
				}
			}
			return false;
		}


		public void setup() {
			Behavior visibility = new Behavior() {
				public Object getValue(Particle particle, Double time, Object param) {
					boolean visibility = (Boolean) param;
					return visibility && isVisible(time);
				}
			};

			Behavior opacity = new Behavior() {
				public Object getValue(Particle particle, Double time, Object param) {
					double opacity = (Double) param;
					double dt = getBehaviorTimeFraction(particle, time);
					double val = PMath.sin(dt * PI);
					return opacity * val;
				}
			};

			Behavior size = new Behavior() {
				public Object getValue(Particle particle, Double time, Object param) {
					DPoint3 size = (DPoint3) param;
					double dt = getBehaviorTime(particle, time);
					double ds = 0.05 + PMath.abs(0.95 * PMath.cos(dt * TWO_PI / BEAT));
					return size.mul(new DPoint3(1, ds, 1));
				}
			};

			addBehavior(Behavior.VISIBILITY, visibility.copy((Double) null, (Double) null));
			for (double t : PPPPPOKERFACE) {
				addBehavior(Behavior.OPACITY, opacity.copy(t, t + (12 * QUICK_BEAT)));
				addBehavior(Behavior.SIZE, size.copy(t, t + (12 * QUICK_BEAT)));
			}
		}
	};

	MegaSphere pip = new MegaSphere(this, 1, 20, 60) {
		private double power = 0;


		public void setPower(double power) {
			this.power = power;
		}


		public void receive(Event event) {
			if (event.getChannel().equals(LIVE_AVERAGE_RMS)) {
				power = ((FloatScalar) event).getValue();
			}
		}


		public boolean isVisible(double time) {
			for (double t : MA_MA) {
				if ((t - QUICK_BEAT <= time) && (time < (t + (4 * QUICK_BEAT) + BEAT))) {
					return true;
				}
			}
			return false;
		}


		public void setup() {
			Behavior visibility = new Behavior() {
				public Object getValue(Particle particle, Double time, Object param) {
					boolean visibility = (Boolean) param;
					return visibility && isVisible(time);
				}
			};

			Behavior opacity = new Behavior() {
				public Object getValue(Particle particle, Double time, Object param) {
					double opacity = (Double) param;
					double dt = getBehaviorTimeFraction(particle, time);
					double val = PMath.sin(dt * PI);
					return opacity * val;
				}
			};

			Behavior size = new Behavior() {
				public Object getValue(Particle particle, Double time, Object param) {
					DPoint3 size = (DPoint3) param;
					double dt = getBehaviorTime(particle, time);
					double ds = 0.05 + PMath.abs(0.95 * PMath.cos(dt * TWO_PI / BEAT));
					return size.mul(new DPoint3(1, ds, 1));
				}
			};

			addBehavior(Behavior.VISIBILITY, visibility.copy((Double) null, null));
			for (double t : MA_MA) {
				double start = t - QUICK_BEAT;
				double end = t + (4 * QUICK_BEAT) + BEAT;
				addBehavior(Behavior.OPACITY, opacity.copy(start, end));
				addBehavior(Behavior.SIZE, size.copy(t, t + (4 * QUICK_BEAT)));
			}
		}
	};

	Pyramid chainLinkShape = new Pyramid(this) {
		private double power = 0;


		public void setPower(double power) {
			this.power = power;
		}


		public void receive(Event event) {
			if (event.getChannel().equals(LIVE_AVERAGE_RMS)) {
				power = ((FloatScalar) event).getValue();
			}
		}


		public void setup() {

			addBehavior(Behavior.VISIBILITY, new Behavior(DANCE_BEAT_1, END_OF_SONG) {
				public Object getValue(Particle particle, Double time, Object param) {
					boolean visibility = (Boolean) param;
					int id = (Integer) particle.getAttribute("id");
					int row = (id / 2) - 1;
					boolean viz = false;
					if (row < 0) {
						viz = true;
					}
					else {
						double poke = PPPPPOKERFACE[row];
						double p0 = poke - BEAT;
						viz = time >= p0;
					}
					return visibility && viz;
				}
			});

			addBehavior(Behavior.OPACITY, new Behavior(START_OF_SONG, END_OF_SONG) {
				public Object getValue(Particle particle, Double time, Object param) {
					double opacity = (Double) param;

					int id = (Integer) particle.getAttribute("id");
					int row = (id / 2) - 1;
					double val = 0;
					if (row < 0) {
						val = 1;
					}
					else {
						double poke = PPPPPOKERFACE[row];
						double p0 = poke - BEAT;
						if (time >= p0) {
							val = (time - p0) / BEAT;
							if (val > 1.0) {
								val = 1.0;
							}
						}
					}
					return opacity * val;
				}
			});

			addBehavior(Behavior.ORIENTATION, new Behavior(DANCE_BEAT_1, END_OF_SONG) {
				public Object getValue(Particle particle, Double time, Object param) {
					DPoint3 orientation = (DPoint3) param;
					double sign = PMath.sign(particle.getPosition().getX());
					return orientation.add(new DPoint3(0, sign * (time - startTime) * TWO_PI * BEAT / 2, 0));
				}
			});

		}
	};

	Grid gridShape = new Grid(this, 20, 20) {
		private double power = 0;


		public void setPower(double power) {
			this.power = power;
		}


		public void receive(Event event) {
			if (event.getChannel().equals(LIVE_AVERAGE_RMS)) {
				power = ((FloatScalar) event).getValue();
			}
		}


		public void setup() {

			addBehavior(Behavior.LINE_WEIGHT, new Behavior(START_OF_SONG, END_OF_SONG) {
				public Object getValue(Particle particle, Double time, Object param) {
					double weight = (Double) param;
					double dw = power / 3;
					return weight + dw;
				}
			});
		}
	};

	Cylinder nobody = new Cylinder(this, 30, false) {
		private double power = 0;


		public void setPower(double power) {
			this.power = power;
		}


		public void receive(Event event) {
			if (event.getChannel().equals(LIVE_AVERAGE_RMS)) {
				power = ((FloatScalar) event).getValue();
			}
		}


		public boolean isVisible(double time) {
			for (double t : LOVE_NOBODY) {
				if ((t - LOVE_NOBODY_BEAT <= time) && (time < (t + LOVE_NOBODY_DURATION + LOVE_NOBODY_BEAT))) {
					return true;
				}
			}
			return false;
		}


		public void setup() {
			Behavior visibility = new Behavior() {
				public Object getValue(Particle particle, Double time, Object param) {
					boolean visibility = (Boolean) param;
					return visibility && isVisible(time);
				}
			};

			Behavior opacity = new Behavior() {
				public Object getValue(Particle particle, Double time, Object param) {
					double opacity = (Double) param;
					double dt = getBehaviorTimeFraction(particle, time);
					double val = PMath.sin(dt * PI);
					return opacity * val;
				}
			};

			Behavior size = new Behavior() {
				public Object getValue(Particle particle, Double time, Object param) {
					DPoint3 size = (DPoint3) param;
					double dt = getBehaviorTime(particle, time);
					double ds = 0.1 + PMath.abs(0.9 * PMath.cos(dt * TWO_PI / LOVE_NOBODY_BEAT));
					return size.mul(new DPoint3(1, ds, 1));
				}
			};

			addBehavior(Behavior.VISIBILITY, visibility.copy((Double) null, null));
			for (double t : LOVE_NOBODY) {
				double start = t - LOVE_NOBODY_BEAT;
				double end = t + LOVE_NOBODY_DURATION + LOVE_NOBODY_BEAT;
				addBehavior(Behavior.OPACITY, opacity.copy(start, end));
				addBehavior(Behavior.SIZE, size.copy(start, end));
			}
		}
	};

	private Particle grid, grid2;

	private Wheel[] centerWheels = new Wheel[2];

	private Wheel[] frameWheels = new Wheel[2];

	private Wheel[] outerWheels = new Wheel[2];

	private List<Wheel> allWheels = new ArrayList<Wheel>();

	private List<Particle> stompers = new ArrayList<Particle>();

	private Particle[] pips = new Particle[2];

	private Particle[] pokers = new Particle[2];

	private Particle[] nobodies = new Particle[2];

	private List<Chain> chains = new ArrayList<Chain>();

	private double power = 0;


	public void receive(Event event) {
		if (event.getChannel().equals(LIVE_AVERAGE_RMS)) {
			power = ((FloatScalar) event).getValue();
		}
	}


	public void setup() {
		int dv = 160;
		if (PRESENT) {
			dv = 150;
		}
		super.setup(16 * dv, 9 * dv, VIDEO_OUTPUT_FILE, OPENGL, EVENT_PLAYBACK_FILE == null, 30.0, AUDIO_FILE, null, 0.0);

		setLineScale((double) PMath.max(width, height) / 1024);

		perspective();
		textFont(createFont("SanSerif", 12));

		setSkewTime(START_OF_SONG);
		setStartTime(START_OF_SONG);
		setEndTime(END_OF_SONG);

		if (EVENT_PLAYBACK_FILE != null) {
			startEventPlayback(EVENT_PLAYBACK_FILE);
		}
		else if (EVENT_RECORDING_FILE != null) {
			startEventRecording(EVENT_RECORDING_FILE);
		}

		addListener(LIVE_AVERAGE_RMS, ball);
		addListener(LIVE_AVERAGE_RMS, stomper);
		addListener(LIVE_AVERAGE_RMS, pip);
		addListener(LIVE_AVERAGE_RMS, poker);
		addListener(LIVE_AVERAGE_RMS, chainLinkShape);
		addListener(LIVE_AVERAGE_RMS, gridShape);
		addListener(LIVE_AVERAGE_RMS, nobody);
		addListener(LIVE_AVERAGE_RMS, this);

		space.setSetup(new Space.Action(space) {
			public void action() {
				double gsize = 6500;
				grid = new Particle(new DPoint3(0, -400, 0));
				grid.setSize(new DPoint3(gsize, gsize, gsize));
				grid.setOrientation(new DPoint3(HALF_PI, 0, 0));
				grid.setSpin(new DPoint3(0, 0, HALF_PI * BEAT / 2));
				grid.setActor(gridShape);
				grid.setLineColor(PColor.MEDIUM_BLUE);
				space.addParticle(grid);

				grid = new Particle(new DPoint3(0, -405, 0));
				grid.setSize(new DPoint3(gsize, gsize, gsize));
				grid.setOrientation(new DPoint3(HALF_PI, 0, 0));
				grid.setSpin(new DPoint3(0, 0, -HALF_PI * BEAT / 2));
				grid.setActor(gridShape);
				grid.setLineColor(PColor.MEDIUM_RED);
				space.addParticle(grid);

				DPoint3 position = new DPoint3(2250, 1250, 0);
				double mass = 2;
				DPoint3 gravity = new DPoint3(0, -80, 0);
				DPoint3 stiffness = new DPoint3(0.5, 0.5, 0.5);
				DPoint3 damping = new DPoint3(0.92, 0.92, 0.92);
				DPoint3 spacing = new DPoint3(0, -CHAIN_LINK_SPACING, 0);
				DPoint3 twist = new DPoint3(0, HALF_PI / (double) CHAIN_LINK_COUNT, 0);
				for (int i = 0; i < CHAIN_COUNT; i++) {
					int dz = 500 - (i * 400);
					Chain c0 = new Chain(CHAIN_LINK_COUNT, position.copy().add(0, 0, dz), spacing, twist, mass, gravity, stiffness, damping);
					chains.add(c0);
					space.addParticle(c0);

					Chain c1 = new Chain(CHAIN_LINK_COUNT, position.copy().mul(-1, 1, 1).add(0, 0, dz), spacing, twist.copy().mul(1, -1, 1), mass, gravity, stiffness, damping);
					chains.add(c1);
					space.addParticle(c1);
				}

				chainLinkShape.setDetail(6);
				int i = 0;
				double si = 1.0 / (double) chains.size();
				for (Chain chain : chains) {
					int dz = 500 - (i * 400);
					int iMod = i % 2;
					double di = si * i;
					chain.setAttribute("id", i);
					chain.setActor(chainLinkShape);
					chain.setSize(new DPoint3(CHAIN_LINK_SIZE, CHAIN_LINK_SIZE, CHAIN_LINK_SIZE));

					PColor lineColor = PColor.RED.darker().scale(1.0f - (float) di);
					chain.setLineColor(lineColor);

					PColor fillColor = PColor.BLUE;
					chain.setFillColor(fillColor);

					DPoint3 start = chain.getPosition().copy();
					DPoint3 end = chain.getPosition().copy().mul(-1, 1, 1);
					DPoint3 deviation = new DPoint3(0, 500, 500);
					for (double glideTime : GLIDE) {
						chain.addActivity(new ChainGlideActivity(glideTime, glideTime + GLIDE_DURATION, start, end, deviation));
					}

					ChainLink link = chain.getDownLink();
					while (link != null) {
						space.addParticle(link);
						link.setAttribute("id", i);
						link.setActor(chainLinkShape);
						link.setSize(new DPoint3(CHAIN_LINK_SIZE, CHAIN_LINK_SIZE, CHAIN_LINK_SIZE));

						lineColor = lineColor.scale(0.95f);
						link.setLineColor(lineColor);

						fillColor = fillColor.scale(0.95f);
						link.setFillColor(fillColor);

						link = link.getDownLink();
					}
					i++;
				}

				// Populate the the wheels
				double speed = 4.0 / (double) COG_COUNT;
				centerWheels[0] = new Wheel(COG_COUNT, ball, new DPoint3(-SQRT2_2 * WHEEL_RADIUS, WHEEL_RADIUS, 0), WHEEL_SIZE.copy(), new DPoint3(0, 1, 0), speed);
				centerWheels[1] = new Wheel(COG_COUNT, ball, new DPoint3(SQRT2_2 * WHEEL_RADIUS, WHEEL_RADIUS, 0), WHEEL_SIZE.copy(), new DPoint3(0, 1, 0), -speed);

				frameWheels[0] = new Wheel(COG_COUNT, ball, new DPoint3(-WHEEL_RADIUS, SQRT2 * WHEEL_RADIUS, 0), WHEEL_SIZE.copy(), new DPoint3(1, 0, 0), -speed);
				frameWheels[1] = new Wheel(COG_COUNT, ball, new DPoint3(WHEEL_RADIUS, SQRT2 * WHEEL_RADIUS, 0), WHEEL_SIZE.copy(), new DPoint3(1, 0, 0), speed);

				outerWheels[0] = new Wheel(COG_COUNT, ball, new DPoint3(-2.0 * WHEEL_RADIUS, WHEEL_RADIUS, 0), WHEEL_SIZE.copy(), new DPoint3(0, 1, 0), speed);
				outerWheels[1] = new Wheel(COG_COUNT, ball, new DPoint3(2.0 * WHEEL_RADIUS, WHEEL_RADIUS, 0), WHEEL_SIZE.copy(), new DPoint3(0, 1, 0), -speed);

				allWheels.addAll(Arrays.asList(centerWheels));
				allWheels.addAll(Arrays.asList(frameWheels));
				allWheels.addAll(Arrays.asList(outerWheels));

				for (Particle p : allWheels) {
					space.addParticle(p);
				}

				int id = 0;
				double sx = (double) 1 / (double) STOMPER_COLUMN_COUNT;
				double sz = (double) 1 / (double) STOMPER_ROW_COUNT;
				for (int x = 0; x <= STOMPER_COLUMN_COUNT; x++) {
					double dx = sx * (double) x;
					double ox = (10000 * dx) - 5000;

					for (int z = 0; z <= STOMPER_ROW_COUNT; z++) {
						double dz = sz * (double) z;
						double oz = -2500 + ((10000 * dz) - 5000);

						Particle p = new Particle(new DPoint3(ox, -100, oz));
						p.setAttribute("id", id);
						p.setAttribute("noise", PMath.random(0.5, 0.75));
						p.setAttribute("x", x);
						p.setAttribute("z", z);
						p.setSize(new DPoint3(STOMPER_SIZE, STOMPER_SIZE / 8, STOMPER_SIZE));
						p.setLineColor(new PColor().fromHSB((float) PMath.random(0.0, 1.0), 1f, 1.0f));
						p.setFillColor(new PColor().fromHSB((float) PMath.random(0.0, 1.0), 1f, 1.0f));
						p.setActor(stomper);
						space.addParticle(p);
						p.setVisible(false);
						stompers.add(p);

						id++;
					}
				}

				pip.setDetail(16);
				pips[0] = new Particle(new DPoint3(-SQRT2_2 * WHEEL_RADIUS, WHEEL_RADIUS, 0));
				pips[1] = new Particle(new DPoint3(SQRT2_2 * WHEEL_RADIUS, WHEEL_RADIUS, 0));
				pips[0].setOrientation(new DPoint3(PI / 2, 0, 0));
				pips[1].setOrientation(new DPoint3(-PI / 2, 0, 0));
				for (Particle p : pips) {
					p.setSize(PIP_SIZE);
					p.setLineColor(new PColor(1, 0.9, 0.5));
					p.setActor(pip);
					space.addParticle(p);
				}

				pokers[0] = new Particle(new DPoint3(-WHEEL_RADIUS, WHEEL_RADIUS + SQRT2 * WHEEL_RADIUS, 0));
				pokers[1] = new Particle(new DPoint3(WHEEL_RADIUS, WHEEL_RADIUS + SQRT2 * WHEEL_RADIUS, 0));
				pokers[0].setOrientation(new DPoint3(PI / 2, 0, 0));
				pokers[1].setOrientation(new DPoint3(-PI / 2, 0, 0));
				for (Particle p : pokers) {
					p.setSize(POKER_SIZE);
					p.setLineColor(new PColor(0.2, 1, 0.2));
					p.setActor(poker);
					space.addParticle(p);
				}

				nobodies[0] = new Particle(new DPoint3(-2.75 * WHEEL_RADIUS, 2 * SQRT2 * WHEEL_RADIUS, 0));
				nobodies[1] = new Particle(new DPoint3(2.75 * WHEEL_RADIUS, 2 * SQRT2 * WHEEL_RADIUS, 0));
				nobodies[0].setOrientation(new DPoint3(-PI / 2, 0, 0));
				nobodies[1].setOrientation(new DPoint3(-PI / 2, 0, 0));
				for (Particle p : nobodies) {
					p.setSize(NOBODY_SIZE);
					p.setLineColor(new PColor(0.3, 0, 0));
					p.setFillColor(new PColor(0.6, 0, 0));
					p.setActor(nobody);
					space.addParticle(p);
				}
			}
		});

		space.setBeforeTick(new Space.Ticker(space) {

			@Override
			public void tick(double time, double deltaTime) {
				for (Wheel wheel : allWheels) {
					wheel.setVisible(false);
				}
				for (Particle p : pips) {
					p.setVisible(false);
				}
				for (Particle p : pokers) {
					p.setVisible(false);
				}
				for (Particle p : nobodies) {
					p.setVisible(false);
				}

				for (Chain chain : chains) {
					chain.setVisible(false);
					ChainLink link = chain.getDownLink();
					while (link != null) {
						link.setVisible(false);

						link = link.getDownLink();
					}
				}

				if ((time >= 0) && (time < GRIND)) {
					double dt = time / GRIND;
					for (Wheel wheel : centerWheels) {
						wheel.setVisible(true);
						wheel.setOpacity(dt);
					}
				}

				if (time >= GRIND) {
					for (Wheel wheel : centerWheels) {
						wheel.setVisible(true);
					}
					for (Particle pip : pips) {
						pip.setVisible(true);
					}
				}

				if ((time >= GLIDE_1) && (time < CHORUS_1_START)) {
					double t2 = CHORUS_1_START - GLIDE_1;
					double dt = (time - GLIDE_1) / t2;
					DPoint3 dFrac = new DPoint3(dt, dt, dt);

					frameWheels[0].setPosition(new Transition.Linear3D(new DPoint3(-SQRT2_2 * WHEEL_RADIUS, WHEEL_RADIUS, 0), new DPoint3(-WHEEL_RADIUS, WHEEL_RADIUS + SQRT2 * WHEEL_RADIUS, 0)).transform(dFrac));
					frameWheels[0].setOpacity(dt);
					frameWheels[0].setVisible(true);

					frameWheels[1].setPosition(new Transition.Linear3D(new DPoint3(SQRT2_2 * WHEEL_RADIUS, WHEEL_RADIUS, 0), new DPoint3(WHEEL_RADIUS, WHEEL_RADIUS + SQRT2 * WHEEL_RADIUS, 0)).transform(dFrac));
					frameWheels[1].setOpacity(dt);
					frameWheels[1].setVisible(true);
				}

				if (time >= DANCE_BEAT_1) {
					for (Particle p : stompers) {
						p.setVisible(true);
					}
					for (Particle poker : pokers) {
						poker.setVisible(true);
					}
					for (Particle p : nobodies) {
						p.setVisible(true);
					}

					for (Chain chain : chains) {
						chain.setVisible(true);
						ChainLink link = chain.getDownLink();
						while (link != null) {
							link.setVisible(true);
							link = link.getDownLink();
						}

						double dt = (time - DANCE_BEAT_1) / (END_OF_SONG - DANCE_BEAT_1);
						DPoint3 pos = chain.getReferencePosition();
						double sign = PMath.sign(pos.getX());
						double ang = sign * HALF_PI * (time - DANCE_BEAT_1) / BEAT;
						double sin = PMath.sin(-ang);
						double cos = PMath.cos(-ang);
						double dx = 2 * CHAIN_LINK_SIZE * sin;
						double dy = 2 * CHAIN_LINK_SIZE * cos;
						double dz = 0;	// 2 * CHAIN_LINK_SIZE * cos;
						chain.setPosition(pos.copy().add(dx, dy, dz));
					}
				}

				if (time >= CHORUS_1_START) {
					frameWheels[0].setOpacity(1);
					frameWheels[0].setVisible(true);

					frameWheels[1].setOpacity(1);
					frameWheels[1].setVisible(true);

					double ang = BEAT * TWO_PI * (time - CHORUS_1_START);

					centerWheels[0].setAngle(ang);
					centerWheels[1].setAngle(-ang);
				}

//				for (int i = 0; i < GLIDE.length; i++) {
//					double glideStart = GLIDE[i];
//					double glideEnd = glideStart + GLIDE_DURATION;
//					if ((glideStart <= time) && (time < glideEnd)) {
//						int mod = i % 2;
//						double dg = (time - glideStart) / GLIDE_DURATION;
//
//						for (Chain chain : chains) {
//							int side = (Integer) chain.getAttribute("side");
//							double sign = (side == 0) ? 1 : -1;
//
//						}
//					}
//				}

				if ((time >= GLIDE_2) && (time < CHORUS_2_START)) {
					double t2 = CHORUS_2_START - GLIDE_2;
					double dt = (time - GLIDE_2) / t2;
					DPoint3 dFrac = new DPoint3(dt, dt, dt);

					outerWheels[0].setPosition(new Transition.Linear3D(new DPoint3(-SQRT2_2 * WHEEL_RADIUS, WHEEL_RADIUS, 0), new DPoint3(-2.0 * WHEEL_RADIUS, WHEEL_RADIUS, 0)).transform(dFrac));
					outerWheels[1].setPosition(new Transition.Linear3D(new DPoint3(SQRT2_2 * WHEEL_RADIUS, WHEEL_RADIUS, 0), new DPoint3(2.0 * WHEEL_RADIUS, WHEEL_RADIUS, 0)).transform(dFrac));

					outerWheels[0].setOpacity(dt);
					outerWheels[1].setOpacity(dt);

					for (Wheel wheel : outerWheels) {
						wheel.setVisible(true);
					}
				}

				if (time >= CHORUS_2_START) {
					for (Wheel wheel : outerWheels) {
						wheel.setVisible(true);
						wheel.setOpacity(1);
					}

					double ang = BEAT * TWO_PI * (time - CHORUS_3_START);

					outerWheels[0].setAngle(ang);
					outerWheels[1].setAngle(-ang);
				}

				if (time >= CHORUS_3_START) {
					double ang = BEAT * TWO_PI * (time - CHORUS_3_START);

					frameWheels[0].setAngle(ang);
					frameWheels[1].setAngle(ang);
				}

				if (time >= LAST_MA) {
					double dt = (time - LAST_MA) / (END_OF_SONG - LAST_MA);
					for (Particle p : space.getParticles()) {
						p.setOpacity(1.0 - dt);
					}
				}
			}
		});
	}


	public void drawDynamic() {
		smooth();
		setCamera();
		setLighting();
		setStyle();

		space.run();
	}


	public void setStyle() {
		background(0);
		noFill();
		ellipseMode(CENTER);
		stroke(PColor.WHITE);
	}


	public void setLighting() {
		directionalLight(.5f, .5f, .5f, 1, 1, -1);
		ambientLight(.25f, .25f, .25f);
		specular(.5);
	}


	public void setCamera() {
		double t = getTime();

		double maxDistance = 3000;
		double normDistance = 1700;

		double distance;
		double beatPeriod = 112d * BEAT;

//		double cameraY = 200;
//		double fov = PI / 1.6;
//		double cameraZ = cameraY / tan(fov / 2.0f);
//		double aspect = 16f/9f;	//(float) width / (float) height;
//		perspective(fov, aspect, cameraZ / 10.0, cameraZ * 100000.0);

		if (LOCK_CAMERA) {
			camera(0, 100, maxDistance, 0, WHEEL_RADIUS, 0, 0f, -1f, 0f);
		}
		else {
			double dt = t / END_OF_SONG;
			distance = maxDistance - (dt * (maxDistance - normDistance));
			swoopCamera(new DPoint3(0, 0, 0), new DPoint3(distance, distance, distance), new DPoint3(), beatPeriod);
		}
	}


	public static class Wheel extends Particle implements Tickable {

		private DPoint3 axis;

		private double angle = 0;

		private double speed;

		private double phase = 0;


		public Wheel(int count, Actor actor, DPoint3 position, DPoint3 size, DPoint3 axis, double speed) {
			super(position);
			setSize(size);
			setAxis(axis);
			setSpeed(speed);

			// Populate the the wheel
			Particle particle = new Particle();
			particle.setSize(new DPoint3(COG_SIZE, COG_SIZE, COG_SIZE));
			particle.setFillColor(PColor.NONE);
			particle.setLineColor(PColor.WHITE);
			particle.setActor(actor);

			for (int i = 0; i < count; i++) {
				Particle p = particle.copy();
				p.setAttribute("id", i);
				double frac = i / (double) count;
				if (speed < 0) {
					frac += 0.5;
					if (frac > 1.0) {
						frac -= 1.0;
					}
				}
				p.setAttribute("frac", frac);
				p.setLineColor(new PColor().fromHSB((float) frac, 1f, 1.0f));
				addChild(p);
			}

		}


		public void tick(double time, double deltaTime) {
			double piTime = (speed * time * TWO_PI) + phase;
			double piStep = TWO_PI / (double) getChildren().size();
			RotationMatrix mx = new RotationMatrix(position, axis, angle);

			for (Particle p : getChildren()) {
				double w = (Integer) p.getAttribute("id");
				double dw = piTime + (w * piStep);
				double px = position.getX() + size.getX() * PMath.cos(dw);
				double py = position.getY() + size.getY() * PMath.sin(dw);
				double pz = position.getZ();
				DPoint3 pos = new DPoint3(px, py, pz);
				pos.from3D(mx.rotate(pos.to3D()));
				p.setPosition(pos);
				p.setOpacity(opacity);
			}

			super.tick(time, deltaTime);
		}


		public DPoint3 getAxis() {
			return axis;
		}


		public void setAxis(DPoint3 axis) {
			this.axis = axis;
		}


		public double getAngle() {
			return angle;
		}


		public void setAngle(double angle) {
			this.angle = angle;
		}


		public double getSpeed() {
			return speed;
		}


		public void setSpeed(double speed) {
			this.speed = speed;
		}


		public double getPhase() {
			return phase;
		}


		public void setPhase(double phase) {
			this.phase = phase;
		}
	}

	public static class ChainGlideActivity extends Activity {

		private Transition.Curve3D transition;

		private boolean started = false;


		public ChainGlideActivity(Double start, Double end, DPoint3 startPt, DPoint3 endPt, DPoint3 deviation) {
			super(start, end);
			transition = new Transition.Curve3D(startPt, endPt, deviation);
		}


		@Override
		public void action(Particle particle, double time) {
			double dt = getTimeframe().getInternalFraction(time);
			if (!started) {
				started = true;
				transition.setStartPoint(particle.getPosition());
			}
			DPoint3 dpt = transition.transform(new DPoint3(dt, dt, dt));
			particle.setPosition(dpt);
		}
	}
}